{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src=\"../../img/ods_stickers.jpg\">\n",
    "## Открытый курс по машинному обучению. Сессия № 3\n",
    "\n",
    "### <center> Автор материала: Михаил Мельник (m.v.melnick@gmail.com)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## <center> Индивидуальный проект по анализу данных </center>\n",
    "### <center> \"Прогнозирование бронирований на AirBnB\" </center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**План исследования**\n",
    " - Описание набора данных и признаков\n",
    " - Первичный анализ признаков\n",
    " - Первичный визуальный анализ признаков\n",
    " - Закономерности, \"инсайты\", особенности данных\n",
    " - Предобработка данных\n",
    " - Создание новых признаков и описание этого процесса\n",
    " - Кросс-валидация, подбор параметров\n",
    " - Построение кривых валидации и обучения \n",
    " - Прогноз для тестовой или отложенной выборки\n",
    " - Оценка модели с описанием выбранной метрики\n",
    " - Выводы\n",
    " \n",
    " Более детальное описание [тут](https://goo.gl/cJbw7V)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Импортируем необходимые библиотеки"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "from matplotlib import pyplot as plt\n",
    "import seaborn as sns\n",
    "from sklearn.preprocessing import LabelBinarizer, StandardScaler, LabelEncoder, OneHotEncoder\n",
    "from sklearn.linear_model import LogisticRegression, LogisticRegressionCV\n",
    "from sklearn.model_selection import train_test_split, GridSearchCV, validation_curve\n",
    "from sklearn.metrics import make_scorer\n",
    "from scipy.sparse import csr_matrix, hstack, vstack\n",
    "import xgboost as xgb\n",
    "from xgboost import XGBClassifier\n",
    "import pickle\n",
    "from sklearn.model_selection import learning_curve\n",
    "\n",
    "%matplotlib inline\n",
    "# figure size in inches\n",
    "import warnings\n",
    "warnings.filterwarnings(module='sklearn*', action='ignore', category=DeprecationWarning)\n",
    "plt.rcParams['figure.figsize'] = 16,9"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def create_submission(predict, id_vals, enc, filename):\n",
    "    # Для каждого ID пользователя отбираем 5 классов с максимальной предсказанной вероятностью\n",
    "    ids = []  # спикок ID пользователей\n",
    "    cts = []  # список стран\n",
    "    for i in range(len(id_vals)):\n",
    "        idx = id_vals[i]\n",
    "        ids += [idx] * 5\n",
    "        cts += enc.inverse_transform(np.argsort(predict[i])[::-1])[:5].tolist()\n",
    "    #Generate submission\n",
    "    sub = pd.DataFrame(np.column_stack((ids, cts)), columns=['id', 'country'])\n",
    "    sub.to_csv(filename, index=False) "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Реализация вычисления целевой метрики NDCG\n",
    "def my_ndcg(y_true, y_predict, k=5):\n",
    "    ndcg = 0\n",
    "    n = 0\n",
    "    for y_true_val, y_predict_vec in zip(y_true, y_predict):\n",
    "        #print(y_true_val, y_predict_vec)\n",
    "        order = np.argsort(y_predict_vec)[::-1][:k]\n",
    "        #print(order)\n",
    "        for idx, y_predict_val in enumerate(order):\n",
    "            if y_true_val == y_predict_val:\n",
    "                #print(1 / np.log2(idx + 2))\n",
    "                ndcg += 1 / np.log2(idx + 2)\n",
    "                break\n",
    "        n += 1\n",
    "    #print(ndcg)\n",
    "    return ndcg / n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ndcg_scorer = make_scorer(my_ndcg, needs_proba=True, k=5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 1. Описание набора данных и признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Введение"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "AirBnB - это онлайн-площадка для размещения, поиска и краткосрочной аренды частного жилья по всему миру. Пользователи Airbnb имеют возможность сдавать путешественникам в аренду своё жильё целиком или частично. Сайт предоставляет платформу для установления контакта между хозяином и гостем, а также отвечает за обработку транзакций."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Данный индивидуальный проект посвящен задаче прогнозирования страны первого бронирования жилья для новых пользователей AirBnb (*задача многоклассовой классификации*). Ценность этой задачи для бизнеса состоит в том, что правильное прогнозирование места первого бронирования позволит делать пользователям персонализированные предложения (баннеры на сайте, рассылка по почте). Это в свою очередь должно снизить среднее время от регистрации нового пользователя до первого бронирования, что повысит конверсию пользователей и благотворно скажется на финансовом результате компании."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Описание набора данных и признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Набор данных получен из Kaggle ([Airbnb New User Bookings](https://www.kaggle.com/c/airbnb-recruiting-new-user-bookings/data)) и включает несколько файлов с данными о пользователях (вкл. социально-демографические данные) и пользовательских веб-сессиях:\n",
    "* *train_users.csv* - обучающая выборка данных о пользователях.\n",
    "\n",
    "|               Переменная | Описание |\n",
    "|--------------------------|----------|\n",
    "|                       id | ID пользователя |\n",
    "|     date_account_created | Дата создания аккаунта на airbnb |\n",
    "|   timestamp_first_active | Дата и время первой активности на сайте airbnb.com |\n",
    "|       date_first_booking | Дата первого бронирования |\n",
    "|                   gender | Пол |\n",
    "|                      age | Возраст |\n",
    "|            signup_method | Способ регистрации (базовый, профиль facebook или google) |\n",
    "|              signup_flow | Страница, с которой пользователь перешел для регистрации |\n",
    "|                 language | Предпочитаемый язык |\n",
    "|        affiliate_channel | Тип рекламы |\n",
    "|       affiliate_provider | Провайдер рекламы |\n",
    "|  first_affiliate_tracked | Первый тип рекламы, с которым взаимодействовал пользователь перед регистрацией |\n",
    "|               signup_app | Приложение, с которого была регистрация |\n",
    "|        first_device_type | Тип первого девайса пользователя |\n",
    "|            first_browser | Первый браузер пользователя |\n",
    "|  **country_destination** | **Целевая переменная** - страна первого бронирования |\n",
    "\n",
    "* *test_users.csv* - тестовая выборка данных о пользователях. Структура аналогична train_users.csv за исключением отсутсвия целевой переменной country_destination.\n",
    "\n",
    "* *sessions.csv* - данные о web-сессиях пользователей (клики, обновление wish-листов, поиск и др.). Будет использоваться для обогащения основных признаков по пользоватлям.\n",
    "\n",
    "|               Переменная |          Описание |\n",
    "|--------------------------|-------------------|\n",
    "|                       id |   ID пользователя |\n",
    "|                   action | Действие на сайте |\n",
    "|              action_type | Тип действия |\n",
    "|            action_detail | Детализация действия |\n",
    "|            device_type | Тип девайса |\n",
    "|            secs_elapsed | Длительность действия |\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Общие замечания по исходным данным"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "* Все пользователи в предоставленных данных из США.\n",
    "* Имеются 12 возможных значений целевой переменной - страны первого бронирования: US (США), FR (Франция), CA (Канада), GB (Великобритания), ES (Испания), IT (Италия), PT (Португалия), NL (Нидерланды), DE (Германия), AU (Австралия), NDF (no destination found), and 'other' (другое).\n",
    "* 'other' (другое) - означает, что пользователь сделал бронирование, но страна бронирования не входит в приведенный выше список. А 'NDF' означает, что первого бронирования не было.\n",
    "* Тестовые и обучающие наборы данных не пересекаются по датам. В тестовом наборе данных нужно спрогнозироать страну первого бронирования для пользователей, у которых первые активности на airbnb (регистрация, поиск, клики) состоялись после 01.07.2014."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 2. Первичный анализ признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Загружаем исходные данные"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train = pd.read_csv('../data/train_users_2.csv')\n",
    "df_test = pd.read_csv('../data/test_users.csv')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train.shape, df_test.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Итак, у нас имеются данные по *213451* пользователям в обучающей выборке и *62096* пользователям в тестовой."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Первичный осмотр данных"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_test.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Выводы по первичному осмотру данных:\n",
    "* По крайней мере в двух признаках (date_first_booking и age) есть пропуски. Далее мы исследуем этот вопрос подробнее.\n",
    "* В наборе данных очень много категориальных признаков. Соответственно, далее их нужно будет преобразовывать.\n",
    "* Признак timestamp_first_active нужно преобразовать к нормальному типу timestamp."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Создадим объединенный набор данных - будет полезно для некоторых дальнейших исследований\n",
    "df_all = pd.concat([df_train, df_test], axis=0, ignore_index=True)\n",
    "# Удалим из объединенного датасета ID пользователя - он пока не нужен\n",
    "df_all.drop('id', axis=1, inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_all.info()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Профилирование категориальных переменных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В нашем наборе данных много категориальных переменных. Давайте исследуем распределение их значений."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "categorical_features = [\n",
    "    'affiliate_channel',\n",
    "    'affiliate_provider',\n",
    "    'country_destination',\n",
    "    'first_affiliate_tracked',\n",
    "    'first_browser',\n",
    "    'first_device_type',\n",
    "    'gender',\n",
    "    'language',\n",
    "    'signup_app',\n",
    "    'signup_method'\n",
    "]\n",
    "\n",
    "for categorical_feature in categorical_features:\n",
    "    df_all[categorical_feature] = df_all[categorical_feature].astype('category')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for cat_feat in categorical_features:\n",
    "    heading = \"Profile for {}\".format(cat_feat)\n",
    "    print(\"*\" * len(heading))\n",
    "    print(heading)\n",
    "    print(\"*\" * len(heading))\n",
    "    print(df_all[cat_feat].value_counts())\n",
    "    print(\"\\n\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Замечания по результатам профилирования категориальных признаков:\n",
    "* В некоторых признаках встречаются значения -unknown- - это очевидно пропущенные значению. Нужно принять это во внимание при очистке данных.\n",
    "* Распределение целевой переменной говорит о том, что подавляющее большинство пользователей (по факту 90%) попадает в две категории - они либо еще не сделали бронирование, либо сделали первое бронирование в США. На это стоит обратить внимание при моделировании, т.к. сильно обобщенная модель может выдавать только два прогноза - NDF и USA."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Переменная возраст (Age)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_all.age.describe()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_all.age.min()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_all.age.max()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_all.age.value_counts()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Мы видим, что в переменной age встречаются странные значения - больше 120 и меньше 10."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sum(df_all.age > 120)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sum(df_all.age < 10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На этапе предобработки такие данные потребуют особого внимания."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Пропущенные значения"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для более объективной картины по пропущенным значениям заменим значения '-unknown-' в некоторых категориальных признаках на стандартное неопределенное значение np.nan."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_all.gender.replace('-unknown-', np.nan, inplace=True)\n",
    "df_train.gender.replace('-unknown-', np.nan, inplace=True)\n",
    "df_test.gender.replace('-unknown-', np.nan, inplace=True)\n",
    "\n",
    "df_all.language.replace('-unknown-', np.nan, inplace=True)\n",
    "df_train.language.replace('-unknown-', np.nan, inplace=True)\n",
    "df_test.language.replace('-unknown-', np.nan, inplace=True)\n",
    "\n",
    "df_all.first_browser.replace('-unknown-', np.nan, inplace=True)\n",
    "df_train.first_browser.replace('-unknown-', np.nan, inplace=True)\n",
    "df_test.first_browser.replace('-unknown-', np.nan, inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Проведем исследование пропущенных значений отдельно для обучающей и тестовой выборки."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train_missing = df_train.isnull().sum() / df_train.shape[0]\n",
    "df_train_missing[df_train_missing > 0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_test_missing = df_test.isnull().sum() / df_test.shape[0]\n",
    "df_test_missing[df_test_missing > 0].drop('date_first_booking')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В наших данных много пропусков в переменных gender, age, date_first_booking и first_browser. \n",
    "Переменная date_first_booking связана с целевой переменной и ее пропуски обусловлены значением NDF целевой переменной. По остальным переменным с пропусками нужно будет выбрать стратегии обработки."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 3. Первичный визуальный анализ признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Распределение целевой переменной"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(16, 4))\n",
    "\n",
    "country_counts = df_train.country_destination.value_counts()\n",
    "country_counts.plot(kind='bar',title='Country Destination Count', ax=axes[0])\n",
    "\n",
    "ax = country_counts.div(country_counts.sum()).plot(kind='bar',title='Country Destination %',ax=axes[1] )\n",
    "ax.set_yticklabels(['{:3.1f}%'.format(x*100) for x in ax.get_yticks()])\n",
    "\n",
    "booked_count = df_train[df_train.country_destination != 'NDF'].country_destination.value_counts()\n",
    "ax1 = booked_count.div(booked_count.sum()).plot(kind='bar',title='Country Destination % без NDF',ax=axes[2] )\n",
    "ax1.set_yticklabels(['{:3.1f}%'.format(x*100) for x in ax1.get_yticks()]);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Графики подтверждают наши наблюдения, сделанные ранее - большая часть значений целевой переменной относятся к двум классам - NDF (не сделавшие бронирование) и USA."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Распределение целевой переменной с учетом пола"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "women = sum(df_train['gender'] == 'FEMALE')\n",
    "men = sum(df_train['gender'] == 'MALE')\n",
    "\n",
    "female_destinations = df_train[df_train['gender'] == 'FEMALE'].country_destination.value_counts() / women * 100\n",
    "male_destinations = df_train[df_train['gender'] == 'MALE'].country_destination.value_counts() / men * 100\n",
    "\n",
    "# Bar width\n",
    "width = 0.4\n",
    "\n",
    "male_destinations.plot(kind='bar', width=width, color='green', position=0, label='Male', rot=0, figsize=(16, 9))\n",
    "female_destinations.plot(kind='bar', width=width, color='pink', position=1, label='Female', rot=0, figsize=(16, 9))\n",
    "\n",
    "plt.legend()\n",
    "plt.xlabel('Destination Country')\n",
    "plt.ylabel('Percentage')\n",
    "\n",
    "sns.despine()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Судя по графику, практически нет различий в предпочтениях мужчин и женщин."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Распределение целевой переменной с учетом возраста"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.distplot(df_train[(df_train.age < 100)&(df_train.age > 10)].age.dropna(), color='#FD5C64')\n",
    "plt.title(\"Распределение пользователей по возрасту\")\n",
    "#plt.figsize = (16, 9)\n",
    "plt.xlabel('Age')\n",
    "plt.rcParams['figure.figsize'] = 16,9"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "AirBnb наиболее популярен среди пользователей от 25 до 40 лет."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "age_data = df_train[(df_train.age < 100)&(df_train.age > 10)]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.boxplot(x='country_destination', y='age', data=age_data, palette=\"muted\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На этом графике не видно больших различий бронирования в зависимости от возраста пользователя."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Попробуем посмотреть на распределение целевой переменной по возрасту на двух частях выборки, разделенных медианой."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "age_data.age.median()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "age = age_data.age.median()\n",
    "\n",
    "younger = sum(age_data['age'] < age)\n",
    "older = sum(age_data['age'] > age)\n",
    "\n",
    "younger_destinations = age_data[age_data['age'] < age].country_destination.value_counts() / younger * 100\n",
    "older_destinations = age_data[age_data['age'] > age].country_destination.value_counts() / older * 100\n",
    "\n",
    "younger_destinations.plot(kind='bar', width=width, color='#A1EA15', position=0, label='Younger 50%', rot=0)\n",
    "older_destinations.plot(kind='bar', width=width, color='#4EE3C9', position=1, label='Older 50%', rot=0)\n",
    "\n",
    "plt.legend()\n",
    "plt.xlabel('Destination Country')\n",
    "plt.ylabel('Percentage')\n",
    "\n",
    "sns.despine()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На этом графике видно, что \"более молодая\" половина делает чаще первое бронирование после регистрации (меньше доля NDF) и в то же время чаще делает бронирование в США."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Временные признаки"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#sns.set_style(\"whitegrid\", {'axes.edgecolor': '0'})\n",
    "#sns.set_context(\"poster\", font_scale=1.1)\n",
    "df_all['date_account_created'] = pd.to_datetime(df_all['date_account_created'])\n",
    "df_all.date_account_created.value_counts().sort_index().plot(kind='line', linewidth=1.2, color='red')\n",
    "plt.title(\"Количество новых пользователей по годам\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Мы видим экспоненциальный рост количества пользователей по времени."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 4. Закономерности, \"инсайты\", особенности данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Выше вы уже отмечали найденные особенности в данных. Соберем их здесь вместе:\n",
    "* Распределение целевой переменной говорит о том, что подавляющее большинство пользователей (по факту 90%) попадает в две категории - они либо еще не сделали бронирование, либо сделали первое бронирование в США. На это стоит обратить внимание при моделировании, т.к. сильно обобщенная модель может выдавать только два прогноза - NDF и USA.\n",
    "* Мы видим экспоненциальный рост количества пользователей по времени. Причем скорость роста усиливается с 2014 года. Возможно имеет смысл использовать для моделирования более свежие данные (начиная с 2014 года).\n",
    "* В данных довольно много пропусков (и некорректных значений) по полу и возрасту. Часть некорректных значений по возрасту можно будет восстановить, т.к. некоторые пользователи вместо возраста ввели год рождения (например, 1951). Пропущенные значения связаны, скорее всего, с нежеланием пользователей раскрывать свои личные данные.\n",
    "* Мы не обнаружили различий в распределении целевой переменной по полу и возрасту. Для моделирования потребуется более глубокий анализ и создание новых признаков.\n",
    "* Пользователи с возрастом меньше медианного (половина выборки) более часто делают первое бронирование, чем вторая половина пользователей.\n",
    "* Переменная date_first_booking связана с целевой переменной и заполнена только в обучающей выборке. Она не может быть использована для обучения модели, поэтому ее следует удалить из обучающей выборки."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 5. Предобработка данных "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Скорректируем форматы переменных, связанных с датами и временем"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train['date_account_created'] = pd.to_datetime(df_train['date_account_created'])\n",
    "df_test['date_account_created'] = pd.to_datetime(df_test['date_account_created'])\n",
    "\n",
    "df_train['timestamp_first_active'] = pd.to_datetime(df_train['timestamp_first_active'], format='%Y%m%d%H%M%S')\n",
    "df_test['timestamp_first_active'] = pd.to_datetime(df_test['timestamp_first_active'], format='%Y%m%d%H%M%S')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Переменная date_first_booking связана с целевой переменной и заполнена только в обучающей выборке, поэтому ее следует удалить:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train.drop('date_first_booking', axis=1, inplace=True)\n",
    "df_test.drop('date_first_booking', axis=1, inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Обработка переменной Age"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Часть пользователей ввели год рождения вместо возраста. Попробуем восстановить их возраст."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_with_year_age_mask = df_train['age'] > 1900\n",
    "df_train.loc[train_with_year_age_mask, 'age'] = 2015 - df_train.loc[train_with_year_age_mask, 'age'] # соревнование 2015г"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_with_year_age_mask = df_test['age'] > 1900\n",
    "df_test.loc[test_with_year_age_mask, 'age'] = 2015 - df_test.loc[test_with_year_age_mask, 'age']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Значения возраста больше 100 считаем некорректными и присваиваем им неопределенное значение."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train.loc[df_train.age >= 100, 'age'] = np.nan\n",
    "df_test.loc[df_test.age >= 100, 'age'] = np.nan"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Аналогично поступаем со значениями возраста менее 15."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train.loc[df_train.age <= 15, 'age'] = np.nan\n",
    "df_test.loc[df_test.age <= 15, 'age'] = np.nan"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Все пропуски в возрасте заполняем -1."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train['age'].fillna(-1, inplace=True)\n",
    "df_test['age'].fillna(-1, inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train['age'] = df_train['age'].astype(int)\n",
    "df_test['age'] = df_test['age'].astype(int)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Обработка остальных пропусков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для категориальных переменных с пропущенными значениями преобразуем пропуски к 'unknown' для того, чтобы потом сработал LabelBinarizer."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train['first_affiliate_tracked'].fillna('unknown', inplace=True)\n",
    "df_test['first_affiliate_tracked'].fillna('unknown', inplace=True)\n",
    "              \n",
    "df_train['first_browser'].fillna('unknown', inplace=True)\n",
    "df_test['first_browser'].fillna('unknown', inplace=True)\n",
    "\n",
    "df_train['language'].fillna('unknown', inplace=True)\n",
    "df_test['language'].fillna('unknown', inplace=True)\n",
    "\n",
    "df_train['gender'].fillna('unknown', inplace=True)\n",
    "df_test['gender'].fillna('unknown', inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 6. Создание новых признаков и описание этого процесса"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Добавим признаки, связанные с сессиями пользователя"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "df_sessions = pd.read_csv('../data/sessions.csv')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_sessions.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_sessions.head(5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Выдвинем гипотезу, что пользоватали, которые чаще пользуются airbnb с мобильного устройства, чаще бронируют жилье в родной стране. Соответственно будет полезно узнать основной используемый девайс в разрезе пользователя."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Определяем основной девайс пользователя\n",
    "sessions_device = df_sessions.loc[:, ['user_id', 'device_type', 'secs_elapsed']]\n",
    "aggregated_lvl1 = sessions_device.groupby(['user_id', 'device_type'], as_index=False, sort=False).aggregate(np.sum)\n",
    "idx = aggregated_lvl1.groupby(['user_id'], sort=False)['secs_elapsed'].transform(max) == aggregated_lvl1['secs_elapsed']\n",
    "df_primary = pd.DataFrame(aggregated_lvl1.loc[idx , ['user_id', 'device_type', 'secs_elapsed']])\n",
    "df_primary.rename(columns = {'device_type':'primary_device', 'secs_elapsed':'primary_secs'}, inplace=True)\n",
    "\n",
    "df_primary = pd.concat([df_primary[['user_id', 'primary_secs']], \n",
    "                        pd.get_dummies(df_primary['primary_device'], \n",
    "                                       prefix='prima')\n",
    "                       ], axis=1)\n",
    "df_primary.columns = [col.replace('-', '').replace(' ', '_').lower() for col in df_primary.columns]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_primary.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Также определим второй по популярности девайс в разрезе пользователя"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Определяем второй девайс пользователя\n",
    "remaining = aggregated_lvl1.drop(aggregated_lvl1.index[idx])\n",
    "idx = remaining.groupby(['user_id'], sort=False)['secs_elapsed'].transform(max) == remaining['secs_elapsed']\n",
    "df_secondary = pd.DataFrame(remaining.loc[idx , ['user_id', 'device_type', 'secs_elapsed']])\n",
    "df_secondary.rename(columns = {'device_type':'secondary_device', 'secs_elapsed':'secondary_secs'}, inplace=True)\n",
    "\n",
    "\n",
    "df_secondary = pd.concat([df_secondary[['user_id', 'secondary_secs']], \n",
    "                        pd.get_dummies(df_secondary['secondary_device'], \n",
    "                                       prefix='second')\n",
    "                       ], axis=1)\n",
    "df_secondary.columns = [col.replace('-', '').replace(' ', '_').lower() for col in df_secondary.columns]\n",
    "#df_secondary = convert_to_binary(df=df_secondary, column_to_convert='secondary_device')\n",
    "#df_secondary.drop('secondary_device', axis=1, inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_secondary.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Дополнительно рассчитаем фичи на основе агрегации различных типов событий из пользовательских сессий."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Вспомогательная функция для расчета агрегатов по типам событий в разрезе клиентов и разворачивания (pivot) типов по столбцам \n",
    "def convert_to_counts(df, id_col, column_to_convert):\n",
    "    id_list = df[id_col].drop_duplicates()\n",
    "\n",
    "    df_counts = df.loc[:,[id_col, column_to_convert]]\n",
    "    df_counts['count'] = 1\n",
    "    df_counts = df_counts.groupby(by=[id_col, column_to_convert], as_index=False, sort=False).sum()\n",
    "\n",
    "    new_df = df_counts.pivot(index=id_col, columns=column_to_convert, values='count')\n",
    "    new_df = new_df.fillna(0)\n",
    "\n",
    "    # Rename Columns\n",
    "    categories = list(df[column_to_convert].drop_duplicates())\n",
    "    for category in categories:\n",
    "        cat_name = str(category).replace(\" \", \"_\").replace(\"(\", \"\").replace(\")\", \"\").replace(\"/\", \"_\").replace(\"-\", \"\").lower()\n",
    "        col_name = column_to_convert + '_' + cat_name\n",
    "        new_df.rename(columns = {category:col_name}, inplace=True)\n",
    "\n",
    "    return new_df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "print(\"Aggregating actions taken...\")\n",
    "session_actions = df_sessions.loc[:, ['user_id', 'action', 'action_type', 'action_detail']]\n",
    "columns_to_convert = ['action', 'action_type', 'action_detail']\n",
    "session_actions = session_actions.fillna('not provided')\n",
    "first = True\n",
    "\n",
    "for column in columns_to_convert:\n",
    "    \n",
    "    print(\"Converting \" + column + \" column...\")\n",
    "    current_data = convert_to_counts(df=session_actions, id_col='user_id', column_to_convert=column)\n",
    "    \n",
    "    # If first loop, current data becomes existing data, otherwise merge existing and current\n",
    "    if first:\n",
    "        first = False\n",
    "        actions_data = current_data\n",
    "    else:\n",
    "        actions_data = pd.concat([actions_data, current_data], axis=1, join='inner')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "actions_data.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Объединим признаки сессий с обучающей и тестовой выборками"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Сначала объединим датасеты с девайсами...\n",
    "df_primary.set_index('user_id', inplace=True)\n",
    "df_secondary.set_index('user_id', inplace=True)\n",
    "device_data = pd.concat([df_primary, df_secondary], axis=1, join=\"outer\")\n",
    "\n",
    "# ...затем к девайсам добавим события\n",
    "combined_results = pd.concat([device_data, actions_data], axis=1, join='outer')\n",
    "df_sessions = combined_results.fillna(0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "primary_cols = [col for col in df_primary.columns if col != 'primary_secs']\n",
    "secondary_cols = [col for col in df_secondary.columns if col != 'secondary_secs']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_sessions.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train.set_index('id', inplace=True)\n",
    "df_test.set_index('id', inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#df_train_sessions = pd.DataFrame(index=df_train['id'])\n",
    "#df_test_sessions = pd.DataFrame(index=df_test['id'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train = df_train.join(df_sessions, how='inner')\n",
    "df_test = df_test.join(df_sessions, how='left')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train.fillna(0, inplace=True)\n",
    "df_test.fillna(0, inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train.shape, df_test.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Выделяем целевую переменную"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_enc = LabelEncoder()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Текстовые значения классов (стран) нужно перекодировать в числовые\n",
    "y = y_enc.fit_transform(df_train[['country_destination']])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train.drop('country_destination', axis=1, inplace=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "categorical_features.remove('country_destination')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Перекодируем текстовые категориальные переменные"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Так как на входе у нах текстовые значения, то используем LabelBinarizer вместо OneHotEncoder\n",
    "lab_enc = LabelBinarizer(sparse_output=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "LabelBinarizer умеет кодировать только один признак за раз..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_affiliate_channel_bin = lab_enc.fit_transform(df_train['affiliate_channel'])\n",
    "X_test_affiliate_channel_bin = lab_enc.transform(df_test['affiliate_channel'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_affiliate_provider_bin = lab_enc.fit_transform(df_train['affiliate_provider'])\n",
    "X_test_affiliate_provider_bin = lab_enc.transform(df_test['affiliate_provider'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_first_affiliate_tracked_bin = lab_enc.fit_transform(df_train['first_affiliate_tracked'])\n",
    "X_test_first_affiliate_tracked_bin = lab_enc.transform(df_test['first_affiliate_tracked'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_first_browser_bin = lab_enc.fit_transform(df_train['first_browser'])\n",
    "X_test_first_browser_bin = lab_enc.transform(df_test['first_browser'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_first_device_type_bin = lab_enc.fit_transform(df_train['first_device_type'])\n",
    "X_test_first_device_type_bin = lab_enc.transform(df_test['first_device_type'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_gender_bin = lab_enc.fit_transform(df_train['gender'])\n",
    "X_test_gender_bin = lab_enc.transform(df_test['gender'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_language_bin = lab_enc.fit_transform(df_train['language'])\n",
    "X_test_language_bin = lab_enc.transform(df_test['language'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_signup_app_bin = lab_enc.fit_transform(df_train['signup_app'])\n",
    "X_test_signup_app_bin = lab_enc.transform(df_test['signup_app'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_signup_method_bin = lab_enc.fit_transform(df_train['signup_method'])\n",
    "X_test_signup_method_bin = lab_enc.transform(df_test['signup_method'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Выделяем признаки, связанные с датами и временем"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train['weekday_account_created'] = df_train.date_account_created.dt.weekday\n",
    "df_test['weekday_account_created'] = df_test.date_account_created.dt.weekday\n",
    "df_train['day_account_created'] = df_train.date_account_created.dt.day\n",
    "df_test['day_account_created'] = df_test.date_account_created.dt.day\n",
    "df_train['month_account_created'] = df_train.date_account_created.dt.month\n",
    "df_test['month_account_created'] = df_test.date_account_created.dt.month\n",
    "df_train['quarter_account_created'] = df_train.date_account_created.dt.quarter\n",
    "df_test['quarter_account_created'] = df_test.date_account_created.dt.quarter\n",
    "df_train['year_account_created'] = df_train.date_account_created.dt.year\n",
    "df_test['year_account_created'] = df_test.date_account_created.dt.year"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train['hour_first_active'] = df_train.timestamp_first_active.dt.hour\n",
    "df_test['hour_first_active'] = df_test.timestamp_first_active.dt.hour\n",
    "df_train['weekday_first_active'] = df_train.timestamp_first_active.dt.weekday\n",
    "df_test['weekday_first_active'] = df_test.timestamp_first_active.dt.weekday\n",
    "df_train['day_first_active'] = df_train.timestamp_first_active.dt.day\n",
    "df_test['day_first_active'] = df_test.timestamp_first_active.dt.day\n",
    "df_train['month_first_active'] = df_train.timestamp_first_active.dt.month\n",
    "df_test['month_first_active'] = df_test.timestamp_first_active.dt.month\n",
    "df_train['quarter_first_active'] = df_train.timestamp_first_active.dt.quarter\n",
    "df_test['quarter_first_active'] = df_test.timestamp_first_active.dt.quarter\n",
    "df_train['year_first_active'] = df_train.timestamp_first_active.dt.year\n",
    "df_test['year_first_active'] = df_test.timestamp_first_active.dt.year"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df_train['time_lag'] = (df_train['date_account_created'] - df_train['timestamp_first_active']).dt.days\n",
    "df_test['time_lag'] = (df_test['date_account_created'] - df_test['timestamp_first_active']).dt.days"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Перекодируем числовые категориальные переменные"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "oh_enc = OneHotEncoder(handle_unknown='ignore')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_cat_bin = oh_enc.fit_transform(df_train[['signup_flow', \n",
    "                                                'weekday_account_created', \n",
    "                                                'day_account_created', \n",
    "                                                'month_account_created', \n",
    "                                                'quarter_account_created', \n",
    "                                                'year_account_created', \n",
    "                                                'hour_first_active', \n",
    "                                                'weekday_first_active', \n",
    "                                                'day_first_active', \n",
    "                                                'month_first_active', \n",
    "                                                'quarter_first_active', \n",
    "                                                'year_first_active']])\n",
    "\n",
    "X_test_cat_bin = oh_enc.transform(df_test[['signup_flow', \n",
    "                                           'weekday_account_created', \n",
    "                                           'day_account_created', \n",
    "                                           'month_account_created', \n",
    "                                           'quarter_account_created', \n",
    "                                           'year_account_created', \n",
    "                                           'hour_first_active', \n",
    "                                           'weekday_first_active', \n",
    "                                           'day_first_active', \n",
    "                                           'month_first_active', \n",
    "                                           'quarter_first_active', \n",
    "                                           'year_first_active']])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_cat_bin.shape, X_test_cat_bin.shape\n",
    "# ((213451, 160), (62096, 160))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Масштабирование признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Так как мы планируем попробовать в т.ч. линейные модели, то необходимо масштабирование признаков."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "scaler = StandardScaler()\n",
    "X_train_age_scaled = scaler.fit_transform(df_train[['age']])\n",
    "X_test_age_scaled = scaler.transform(df_test[['age']])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_time_lag_scaled = scaler.fit_transform(df_train[['time_lag']])\n",
    "X_test_time_lag_scaled = scaler.transform(df_test[['time_lag']])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_primary_secs_scaled = scaler.fit_transform(df_train[['primary_secs']])\n",
    "X_test_primary_secs_scaled = scaler.transform(df_test[['primary_secs']])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_actions_scaled = scaler.fit_transform(df_train[actions_data.columns])\n",
    "X_test_actions_scaled = scaler.transform(df_test[actions_data.columns])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Объединим все созданные признаки в единый датасет"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train = csr_matrix(\n",
    "            hstack(\n",
    "                [X_train_affiliate_channel_bin,\n",
    "                 X_train_affiliate_provider_bin,\n",
    "                 X_train_first_affiliate_tracked_bin,\n",
    "                 X_train_first_browser_bin,\n",
    "                 X_train_first_device_type_bin,\n",
    "                 X_train_gender_bin,\n",
    "                 X_train_language_bin,\n",
    "                 X_train_signup_app_bin,\n",
    "                 X_train_signup_method_bin,\n",
    "                 X_train_cat_bin,\n",
    "                 X_train_age_scaled,\n",
    "                 X_train_time_lag_scaled,\n",
    "                 X_train_actions_scaled,\n",
    "                 df_train[primary_cols]\n",
    "                ]\n",
    "            )\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_test = csr_matrix(\n",
    "            hstack(\n",
    "                [X_test_affiliate_channel_bin,\n",
    "                 X_test_affiliate_provider_bin,\n",
    "                 X_test_first_affiliate_tracked_bin,\n",
    "                 X_test_first_browser_bin,\n",
    "                 X_test_first_device_type_bin,\n",
    "                 X_test_gender_bin,\n",
    "                 X_test_language_bin,\n",
    "                 X_test_signup_app_bin,\n",
    "                 X_test_signup_method_bin,\n",
    "                 X_test_cat_bin,\n",
    "                 X_test_age_scaled,\n",
    "                 X_test_time_lag_scaled,\n",
    "                 X_test_actions_scaled,\n",
    "                 df_test[primary_cols]\n",
    "                ]\n",
    "            )\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train.shape, X_test.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Выделяем обучающую выборку и валидационную (отложенную)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train_part, X_valid, y_train_part, y_valid = train_test_split(X_train, y, test_size=0.3, random_state=17)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Часть 7. Выбор метрики"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В данной задаче по каждому пользователю из тестовой выборки нам нужно спрогнозировать 5 стран, разместив прогнозы в порядке уменьшения вероятности первого бронирования. В идеальном прогнозе для каждого пользователя на первом месте будет идти \"истинная\" страна первого бронирования. В хорошем же прогнозе чем выше \"истинная\" страна в списке из 5 спрогнозированных стран, тем лучше. Это чем-то похоже на ранжирование поисковой выдачи, поэтому применяется соответствующая метрика качества.\n",
    "\n",
    "В качестве основной метрики для оценки результатов модели мы будем использовать (вслед за организаторами соответствующего соревнования Kaggle) метрику [NDCG (Normalized discounted cumulative gain)](https://en.wikipedia.org/wiki/Discounted_cumulative_gain). Это метрика качества ранжирования, которая широко используется в информационном поиске, а также в некоторых задачах многолассовой классификации (наш случай). NDCG является нормализованной (нормированной) версией метрики Discounted Cumulative Gain (DCG), которая определяется следующим образом:\n",
    "$$\\large \\begin{array}{rcl}\n",
    "DCG_k = \\sum_{i=1}^{k}\\dfrac{2^{rel_i}-1}{\\log_{2}(i+1)}\n",
    "\\end{array}$$\n",
    "\n",
    "$rel_i$ - это релевантность нахождения объекта поисковой выдачи на позиции $i$. В нашем случае \"истинная\" страна имеет $rel_i = 1$, а все остальные $rel_i = 0$. Поэтому чем выше истинная страна в ранкинге, тем выше скор DCG.\n",
    "$k$ - параметр метрики - количество объектов, которые рассматриваются в ранкинге. В нашем случае $k = 5$.\n",
    "\n",
    "Для получения NDCG нужно DCG разделить на \"идеальный\" DCG для набора документов, которые все правильно отсортированы по их релевантности. В нашем случае IDCG = 1, т.к. у нас только одна релевантная страна и она должна быть расположена на первом месте, следовательно $\\dfrac{2^{1}-1}{\\log_{2}(1+1)}=1$."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Часть 8. Выбор модели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Так как мы имеем дело с задачей многоклассовой классификации, то попробовуем использовать два класса моделей: \n",
    "* Логистическую регрессию (LogisticRegression) - простой и быстрый вариант.\n",
    "* Градиентный бустинг (XGBoost) - потенциально обеспечит более высокую точность."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 9. Кросс-валидация, подбор параметров"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### _Логистическая регрессия_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Для начала построим модель логистической регрессии с параметрами по умолчанию."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "clf_logit = LogisticRegression(random_state=17, \n",
    "                               multi_class='multinomial',\n",
    "                               n_jobs=4,\n",
    "                               solver='lbfgs'\n",
    "                              )\n",
    "clf_logit.fit(X_train_part, y_train_part)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Прогноз для отложенной и тестовой выборки"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_predict = clf_logit.predict_proba(X_valid)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "my_ndcg(y_valid, y_predict, k=5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "clf_logit.fit(X_train, y)\n",
    "y_predict = clf_logit.predict_proba(X_test)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "create_submission(y_predict, df_test.index, y_enc, 'sub_log_base.csv')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Данный прогноз дает показатель на public LB = 0.85764, а на private LB - 0.86166."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Кросс-валидация логистической регрессии"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь выполним кросс-валидацию модели логистической регрессии. Будем подбирать параметр регуляризации $C$ на кросс-валидации по 5 фолдам."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "C_vals = np.logspace(-2, 2, 5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "log_cv = LogisticRegressionCV(Cs=C_vals, \n",
    "                              cv=5, \n",
    "                              random_state=17, \n",
    "                              multi_class='multinomial', \n",
    "                              solver='lbfgs', \n",
    "                              scoring=ndcg_scorer, \n",
    "                              n_jobs=-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "log_cv.fit(X_train_part, y_train_part)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "log_cv.C_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Оптимальный параметр C = 0.01."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Прогноз для отложенной и тестовой выборки (логистическая регрессия)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Выполним прогнозы с учетом оптимального значения параметра C, полученного на кросс-валидации."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "clf_logit = LogisticRegression(C=0.01, \n",
    "                               random_state=17, \n",
    "                               n_jobs=4,\n",
    "                               multi_class='multinomial', \n",
    "                               solver='lbfgs')\n",
    "clf_logit.fit(X_train_part, y_train_part)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_predict = clf_logit.predict_proba(X_valid)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "my_ndcg(y_valid, y_predict, k=5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "clf_logit.fit(X_train, y)\n",
    "y_predict = clf_logit.predict_proba(X_test)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "create_submission(y_predict, df_test.index, y_enc, 'sub_tmp_cv.csv')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "По результатам кросс-валидации удалось улучшить скор на public LB - до 0.86439, на private LB - 0.86825."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Построим кривые валидации"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "C_vals = np.logspace(-2, 1, 5)\n",
    "clf_logit = LogisticRegression(random_state=17, \n",
    "                               n_jobs=-1,\n",
    "                               multi_class='multinomial', \n",
    "                               solver='lbfgs')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "val_train, val_test = validation_curve(clf_logit, \n",
    "                                       X_train, \n",
    "                                       y, \n",
    "                                       param_name='C', \n",
    "                                       param_range=C_vals, \n",
    "                                       cv=5,\n",
    "                                       scoring='neg_log_loss',\n",
    "                                       n_jobs=-1,\n",
    "                                       verbose=1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_with_err(x, data, **kwargs):\n",
    "    mu, std = data.mean(1), data.std(1)\n",
    "    lines = plt.plot(x, mu, '-', **kwargs)\n",
    "    plt.fill_between(x, mu - std, mu + std, edgecolor='none',\n",
    "                     facecolor=lines[0].get_color(), alpha=0.2)\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.rcParams['figure.figsize'] = 16,9\n",
    "plot_with_err(C_vals, val_train, label='training scores')\n",
    "plot_with_err(C_vals, val_test, label='validation scores')\n",
    "plt.xlabel(r'$C$'); plt.ylabel('log loss')\n",
    "plt.legend();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На основе анализа кривых валидации можно сделать вывод, что наша модель переобучена, т.к. в LogisticRegression меньшим значениям \"C\" соответствует большая регуляризация."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Построим кривые обучения"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import log_loss"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "log_loss_scorer = make_scorer(lambda y_true, y_pred: log_loss(y_true, y_pred, labels=np.unique(y_true)), needs_proba=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "clf_logit = LogisticRegression(random_state=17, \n",
    "                               n_jobs=4,\n",
    "                               C=0.01,\n",
    "                               multi_class='multinomial', \n",
    "                               solver='lbfgs')\n",
    "    \n",
    "N_train, val_train, val_test = learning_curve(clf_logit,\n",
    "                                              X_train, y, \n",
    "                                              #groups=y,\n",
    "                                              #train_sizes=train_sizes, \n",
    "                                              cv=3,\n",
    "                                              n_jobs=1, \n",
    "                                              verbose=1,\n",
    "                                              scoring=ndcg_scorer)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.rcParams['figure.figsize'] = 16,9\n",
    "plot_with_err(N_train, val_train, label='training scores')\n",
    "plot_with_err(N_train, val_test, label='validation scores')\n",
    "plt.xlabel('Training Set Size'); plt.ylabel('log loss')\n",
    "plt.title(\"Learning curves\")\n",
    "plt.legend()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На кривой обучения мы видим, что метрики качества на обучающающей и валидационной выборках сходятся после примерно 15000 наблюдений. Далее они практически не меняются."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### _Градиентный бустинг (XGBoost)_"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Подбираем базовое количество деревьев"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "XGBoost имеет большое количество тонких настроек. Вначале мы попробуем подобрать оптимальное количество деревьев (n_estimators) при помощи встроенной функции кросс-валидации xgboost. Из-за ограничения в вычислительных ресурсах мы не будем проверять значения n_estimators больше 100."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Кастомная реализация метрики NDCG\n",
    "def ndcg5_score(preds, dtrain):\n",
    "    labels = dtrain.get_label()\n",
    "    top = []\n",
    "\n",
    "    for i in range(preds.shape[0]):\n",
    "        top.append(np.argsort(preds[i])[::-1][:5])\n",
    "\n",
    "    mat = np.reshape(np.repeat(labels,np.shape(top)[1]) == np.array(top).ravel(),np.array(top).shape).astype(int)\n",
    "    score = np.mean(np.sum(mat/np.log2(np.arange(2, mat.shape[1] + 2)),axis = 1))\n",
    "    return 'ndcg5', score"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "xgtrain = xgb.DMatrix(X_train_part, label=y_train_part)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "param = {\n",
    "    'learning_rate': 0.1,\n",
    "    'max_depth': 5,\n",
    "    'n_estimators': 100,\n",
    "    'objective': 'multi:softprob',\n",
    "    'num_class': 12,\n",
    "    'gamma': 0,\n",
    "    'min_child_weight': 1,\n",
    "    'subsample': 0.8,\n",
    "    'colsample_bytree': 0.8,\n",
    "    'scale_pos_weight': 1,\n",
    "    'nthread': 8,\n",
    "    'n_jobs': -1,\n",
    "    'seed': 17\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Запускаем встроенную функцию кросс-валидации xgboost"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "cvresult = xgb.cv(param, \n",
    "                  xgtrain, \n",
    "                  num_boost_round=100, \n",
    "                  metrics=['mlogloss'], \n",
    "                  feval=ndcg5_score, \n",
    "                  maximize=True,\n",
    "                  #nfold=5,\n",
    "                  seed=17, \n",
    "                  early_stopping_rounds=50, \n",
    "                  verbose_eval=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cvresult.shape[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Мы уперлись в ограничение на количество деревьев. Возможно, имеется потенциал дальнейшего улучшения, но из-за вычислительных ограничений мы остановимся на n_estimators=100."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Прогноз на отложенной выборке"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "xgb1 = XGBClassifier(\n",
    "     learning_rate=0.1,\n",
    "     min_child_weight=1,\n",
    "     n_estimators=100,\n",
    "     max_depth=5,\n",
    "     gamma=0,\n",
    "     subsample=0.8,\n",
    "     colsample_bytree=0.8,\n",
    "     objective='multi:softprob',\n",
    "     num_class=12,\n",
    "     scale_pos_weight=1,\n",
    "     reg_alpha=0,\n",
    "     reg_lambda=1,\n",
    "     nthread=8,\n",
    "     n_jobs=-1,\n",
    "     seed=17\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "xgb1.fit(X_train_part, y_train_part)\n",
    "y_predict = xgb1.predict_proba(X_valid)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "my_ndcg(y_valid, y_predict, k=5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "xgb1.fit(X_train, y)\n",
    "y_predict = xgb1.predict_proba(X_test)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "create_submission(y_predict, df_test.index, y_enc, 'sub_xgb_100.csv')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Данный сабмит получил public LB - 0.88039, private LB - 0.88552"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Настройка параметров XGBoost"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Из-за ограничения вычислительных ресурсов и особенностей пространства признаков задачи (много разреженных переменных) сделать полноценный поиск оптимальных параметров XGBoost не представляется возможным. Нам не удалось за разумное время посчитать при помощи GridSearch и кросс-валидации на 3 фолда оптимальные параметры, поэтому мы использовали более простую схему ручного перебора - без кросс-валидации, но с оценкой на валидационной (отложенной) выборке. В результате получены следующие \"оптимальные\" параметры:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "xgb1 = XGBClassifier(\n",
    "     learning_rate=0.1,\n",
    "     min_child_weight=1,\n",
    "     n_estimators=100,\n",
    "     max_depth=5,\n",
    "     gamma=0.5,\n",
    "     subsample=0.8,\n",
    "     colsample_bytree=0.8,\n",
    "     objective='multi:softprob',\n",
    "     num_class=12,\n",
    "     scale_pos_weight=1,\n",
    "     reg_alpha=0,\n",
    "     reg_lambda=1,\n",
    "     nthread=8,\n",
    "     n_jobs=-1,\n",
    "     seed=17\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Прогноз на отложенной выборке"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "xgb1.fit(X_train_part, y_train_part)\n",
    "y_predict = xgb1.predict_proba(X_valid)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "my_ndcg(y_valid, y_predict, k=5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Прогноз на тестовой выборке и сабмит"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "xgb1.fit(X_train, y)\n",
    "y_predict = xgb1.predict_proba(X_test)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "create_submission(y_predict, df_test.index, y_enc, 'sub_xgb_100_tune.csv')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Данный сабмит получил public LB - 0.88052, private LB - 0.88559."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Часть 11. Выводы "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "В результате применения более сложного семейства моделей - градиентного бустинга - нам удалось повысить качество прогноза на тестовой выборке, однако это было сделано ценой значительно больших вычислительных ресурсов и времени. Целевая метрика получилась довольно высокой 0.88559 (private LB), что говорит о хорошем качестве прогноза. На момент выполнения проекта соревнование на Kaggle уже было завершено, но по текущим показателям в лидерборде мой сабмит находится на 95 месте (top 6%).\n",
    "\n",
    "Результаты прогноза наиболее вероятной страны первого бронирования позволят оптимизировать показ рекламных баннеров и почтовую рассылку, сделает ее более персонализированной. Это в свою очередь позволит увеличить степень использования сервиса новыми клиентами и снизить среднее время от регистрации до первого бронирования."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Если у вас возникнут вопросы, замечания или вы хотите поделиться своими мыслями по поводу этого проекта и подходов описанных в нем, то в ODS slack вы можете меня найти @konobey**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
